<template>
  <div class="webrtc-demo">
    <el-row :gutter="20">
      <el-col :span="6">
        <div style="width: 100%; height: 800px">
          <ul v-for="(item, index) in roomUserList" :key="index">
            <el-tag size="mini" @click="getStats" type="success">{{
              "用户" + item.nickname
            }}</el-tag>
            <el-tag
              v-if="userInfo.userId === item.userId"
              type="danger"
              size="mini"
              @click="changeBitRate()"
              >增加比特率</el-tag
            >
            <el-button
              size="mini"
              type="primary"
              v-if="userInfo.userId !== item.userId"
              @click="call(item)"
              >通话</el-button
            >
            <el-button
              v-if="userInfo.userId === item.userId"
              size="mini"
              type="danger"
              @click="openVideoOrNot"
              >切换</el-button
            >
          </ul>
        </div>
      </el-col>
      <el-col :span="18">
        <el-row>
          <div
            style="
              width: 800px;
              height: 200px;
              display: flex;
              flex-direction: row;
              align-items: center;
            "
          >
            <el-form
              :model="formInline"
              label-width="250px"
              class="demo-form-inline"
            >
              <el-form-item label="发送消息">
                <el-input v-model="formInline.rtcmessage" placeholder="消息" />
              </el-form-item>
              <el-form-item label="远端消息">
                <div>{{ formInline.rtcmessageRes }}</div>
              </el-form-item>

              <el-form-item>
                <el-button type="primary" @click="sendMessageUserRtcChannel"
                  >点击发送</el-button
                >
              </el-form-item>
            </el-form>
          </div>
        </el-row>
        <el-row>
          <div
            style="
              display: flex;
              flex-direction: row;
              justify-content: flex-start;
            "
          >
            <video
              @click="streamInfo('localdemo01')"
              id="localdemo01"
              autoplay
              controls
              muted
            ></video>
            <video
              @click="streamInfo('remoteVideo01')"
              id="remoteVideo01"
              autoplay
              controls
              muted
            ></video>
          </div>
        </el-row>
      </el-col>
    </el-row>
  </div>
</template>

<script setup>
import { io } from "socket.io-client";
const linkSocket = ref(undefined);
const rtcPcParams = ref({
  iceServes: [{ url: "stun:stun.l.google.com:19302" }], //谷歌的公共服务
});
const roomUserList = ref([]);
const userInfo = ref({});
const localRtcPc = ref(undefined);
const rtcmessage = ref(undefined); //响应
const mapSender = ref([]); //发送的媒体
const channel = ref(undefined);
const formInline = reactive({
  rtcmessage: undefined,
  rtcmessageRes: undefined, //响应
});

/*
  handleError函数用于处理媒体设备获取错误的情况，并进行相应的处理和通知。
  参数：
    - error: 表示捕获到的错误对象
  错误处理步骤：
    1. 通过Element UI的Notification组件向用户显示错误通知，通知标题为"Error"，消息内容为带有样式的<i>标签，提醒用户检查摄像头设备是否正常工作。
    2. 将捕获到的错误信息输出到控制台，包括错误消息(error.message)和错误名称(error.name)。
*/
const handleError = (error) => {
  ElNotification({
    title: "Error",
    message: h(
      "i",
      { style: "color: teal" },
      "摄像头无法正常使用，请检查是否占用或缺失"
    ),
  });
  console.error(
    "navigator.MediaDevices.getUserMedia error: ",
    error.message,
    error.name
  );
};

/**
 * 根据浏览器支持情况获取PeerConnection对象
 */
var PeerConnection =
  window.RTCPeerConnection ||
  window.mozRTCPeerConnection ||
  window.webkitRTCPeerConnection;
console.log("PeerConnection", PeerConnection);
/**
 *
 * @param {*} queryName
 * 解析URL中的参数，并返回对应的值。
 * 参数：
 *  - queryName: 要获取的参数名称。
 * 返回值：
 *  - 返回与queryName参数对应的值。如果参数不存在，则返回null。
 */
const getParams = (queryName) => {
  let url = window.location.href;
  let query = decodeURI(url.split("?")[1]);
  let vars = query.split("&");
  for (var i = 0; i < vars.length; i++) {
    var pair = vars[i].split("=");
    if (pair[0] === queryName) {
      return pair[1];
    }
  }
  return null;
};

/**
 * 初始化函数,接收三个参数：userId、roomId和nickname
 * @param userId
 * @param roomId
 * @param nickname
 */
const init = (userId, roomId, nickname) => {
  // 将userInfo的值设置为一个对象，包含userId、roomId和nickname
  userInfo.value = {
    userId: userId,
    roomId: roomId,
    nickname: nickname,
  };

  // 将linkSocket的值设置为一个io对象，连接到http://localhost:3000，并设置重连最大延迟为10000，传输协议为websocket，查询参数为userId、roomId和nickname
  linkSocket.value = io("http://localhost:18080", {
    reconnectionDelayMax: 10000,
    transports: ["websocket"],
    query: {
      userId: userId,
      roomId: roomId,
      nickname: nickname,
    },
  });
  /**
   * .on()方法接收两个参数
   * 第一个参数是事件名称，第二个参数是回调函数
   * 当客户端接收到与指定事件名称匹配的消息的时候，将调用该回调函数
   */
  // 监听linkSocket的connect事件，连接成功时打印linkSocket的值
  linkSocket.value.on("connect", (e) => {
    console.log("server init connect success", linkSocket.value);
  });

  // 监听linkSocket的roomUserList事件，收到事件时打印事件内容，并将roomUserList的值设置为事件内容
  linkSocket.value.on("roomUserList", (e) => {
    console.log("roomUserList", e);
    roomUserList.value = e;
  });

  // 监听linkSocket的msg事件，收到事件时打印事件内容
  linkSocket.value.on("msg", async (e) => {
    console.log("msg", e);

    // 如果事件类型为join或leave，则延时1000毫秒后重新获取房间用户列表
    if (e["type"] === "join" || e["type"] === "leave") {
      setTimeout(() => {
        let params = { roomId: getParams("roomId") };
        /**
         * .emit() 方法接收两个参数
         * 要发送的事件名称和要发送的数据。
         * 会将指定的事件和数据发送给服务器。服务器可以通过监听相同的事件来接收数据这些消息，并在接收到消息时候执行相应的操作
         */
        linkSocket.value.emit("roomUserList", params);
      }, 1000);
    }

    // 如果事件类型为call，则调用onCall函数
    if (e["type"] === "call") {
      await onCall(e);
    }
    // 如果事件类型为offer，则调用onRemoteOffer函数
    if (e["type"] === "offer") {
      await onRemoteOffer(e["data"]["userId"], e["data"]["offer"]);
    }
    // 如果事件类型为answer，则调用onRemoteAnswer函数
    if (e["type"] === "answer") {
      await onRemoteAnswer(e["data"]["userId"], e["data"]["answer"]);
    }
    // 如果事件类型为candidate，则将candidate添加到localRtcPc中
    if (e["type"] === "candidate") {
      localRtcPc.value.addIceCandidate(e.data.candidate);
    }
  });

  // 监听linkSocket的error事件，收到事件时打印事件内容
  linkSocket.value.on("error", (e) => {
    console.log("error", e);
  });
};

/**
 *
 * 获取本地媒体流
 * @returns {Promise<MediaStream | void>}
 * 返回一个Promise对象，如果成功，则resolve一个MediaStream对象，表示本地媒体流；如果失败，则reject一个错误对象。
 * 语法：var promise = navigator.mediaDevices.getUserMedia(constraints)
 */
const getLocalUserMedia = async () => {
  return navigator.mediaDevices.getUserMedia(constraints).catch(handleError);
};
/**
 * 将新的视频流设置到video元素上
 * @param {string} domId
 * @param {MediaStream} newStream
 * @returns {Promise<void>}
 */
const setDomVideoStream = async (domId, newStream) => {
  let video = document.getElementById("domId");
  let stream = video.srcObject;
  if (stream) {
    /**
     * getAudioTracks() 方法返回一个包含所有音频轨道的数组。
     * getVideoTracks() 方法返回一个包含所有视频轨道的数组。
     * removeTrack() 方法从 MediaStream 中移除指定的轨道。
     * 这里将音频轨道和视频轨道从 MediaStream 中移除，因为它们已经不再需要。
     * 这样做可以释放资源并提高性能。
     */
    stream.getAudioTracks().forEach((e) => {
      stream.removeTrack(e);
    });

    stream.getVideoTracks().forEach((e) => {
      stream.removeTrack(e);
    });
  }
  // 新的视频流会赋值给新的video
  video.srcObject = newStream;
  // 将视频静音
  video.muted = true;
};

/**
 * 设置远程视频流
 * @param {string} domId -视频元素的id属性
 * @param {MediaStreamTrack} track -要添加到视频流中的媒体轨道
 */
const setRemoteDemoVideoStream = (domId, track) => {
  let video = document.getElementById(domId);
  let stream = video.srcObject;
  if (stream) {
    /**
     * addTrack() 给流添加一个新的轨道
     */
    stream.addTrack(track);
  } else {
    let newStream = new MediaStream();
    newStream.addTrack(track);
    video.srcObject = newStream;
    video.muted = true;
  }
};

const call = async (item) => {
  initCallerInfo(getParams("userId"), item.userId);
  let params = {
    userId: getParams("userId"),
    targetUserId: item.userId,
  };
  linkSocket.value.emit("call", params);
};

const onCall = async (e) => {
  console.log("远程呼叫", e);
  await initCalleeInfo(e.data["targetUserId"], e.data["userId"]);
};

const initCalleeInfo = async (localUid, formUid) => {
  //   初始化pc
  localRtcPc.value = new RTCPeerConnection();
  //   初始化本地媒体信息
  let localStream = await getLocalUserMedia({ audio: true, video: true });
  /**
   * getTracks()方法
   * 返回一个序列（通常是一个数组），其中包含了该流中所有的 MediaStreamTrack 对象，不论这些轨道的类型是什么
   */
  for (const track of localStream.getTracks()) {
    localRtcPc.value.addTrack(track);
  }

  await setDomVideoStream("localdemo01", localStream);

  // 监听ICE候选项
  onPcEvent(localRtcPc.value, localUid, formUid);
};

const initCallerInfo = async (callerId, calleeId) => {
  mapSender.value = [];
  // 初始化
  localRtcPc.value = new RTCPeerConnection();
  // 获取本地媒体并添加到pc
  let localStream = await getLocalUserMedia({ audio: true, video: true });

  for (const track of localStream.getTracks()) {
    mapSender.value.push(localRtcPc.value.addTrack(track));
  }

  // 本地渲染
  await setDomVideoStream("localdemo01", localStream);

  // 回调监听
  onPcEvent(localRtcPc.value, callerId, calleeId);
  // 创建offer 生成SDP
  /**
   * createOffer()方法创建生成SDP.SDP是一种会话信息的协议
   */
  let offer = await localRtcPc.value.createOffer();
  // 设置offer为本地描述
  /**
   * setLocalDescription()方法用于将远端的SDP设置本地SDP
   */
  await localRtcPc.value.setLocalDescription(offer);
  // 发送offer给被呼叫端
  let params = { targetUid: calleeId, userId: callerId, offer: offer };

  linkSocket.value.emit("offer", params);
};
const onPcEvent = async (pc, localUid, formUid) => {
  /**
   * createDataChannel()方法
   * 创建一个专门用于数据传输通道，可以用来发送文本，文件，游戏数据等
   * 使用 createDataChannel() 方法创建数据通道时，可以指定一些配置参数，
   * 如通道的名称、是否有可靠传输等。创建完成后，可以通过这个数据通道进行数据的发送和接收。
   */
  channel.value = pc.createDataChannel("chat");
  /**
   * 当远程端添加轨道到 RTCPeerConnection 时触发
   * 通常发生在对等连接建立后，远程端开始发送音视频数据流给本地端时。
   * 你可以通过监听这个事件来获取远程音视频流，并将它们渲染到本地页面上。
   */
  pc.ontrack = function (event) {
    console.log("event", event);
    setRemoteDemoVideoStream("remoteVideo01", event.track);
  };

  /**
   * 当需要协商（negotiation）新的连接时触发。
   * 这可能是由于本地端需要创建一个 offer 或者 answer，以便与远程端建立连接。
   */
  pc.onnegotiationneeded = function (event) {
    console.log("onnegotiationneeded重新协商", event);
  };
  /**
   * 当一个新的数据通道被创建时触发。数据通道用于在对等连接之间传输任意数据，例如文本消息、文件等
   * 可以在这个事件回调中处理数据通道的相关逻辑，例如设置消息监听器、发送数据等。
   */
  // 这段代码主要用于监听数据通道事件，并在不同事件发生时执行相应的逻辑处理，以便实现数据通道的管理和消息传输功能。
  pc.ondatachannel = function (event) {
    console.log("ondatachannel", event);
    // 数据通道打开调用回调函数，处理数据通道打开时的逻辑
    event.channel.onopen = function () {
      console.log("Data channel--------open-------");
    };
    // 当接收到新消息时候调用回调函数，处理接收到的消息
    event.channel.onmessage = function (data) {
      console.log("Data channel--------message-------", data);
      formInline.rtcmessageRes = data.data;
    };
    // 数据通道关闭时候调用回调函数，用于处理数据通道关闭时候的逻辑
    event.channel.onclose = function () {
      console.log("Data channel--------close-------");
    };
  };
  /**
   * 当 ICE 候选（ICE candidate）可用时触发
   * ICE 是用于对等连接中的网络地址和端口协商的技术，ICE 候选包含了网络信息，用于对等连接的建立。
   * 你可以在这个事件回调中收集 ICE 候选，并通过信令服务器将它们发送给对方，以便建立对等连接。
   */
  pc.onicecandidate = async (event) => {
    if (event.candidate) {
      linkSocket.value.emit("candidate", {
        targetUid: remoteUid,
        userId: localUid,
        candidate: event.candidate,
      });
    }
  };
};

const sendMessageUserRtcChannel = () => {
  if (!channel.value) {
    console.log("channel", channel);
    ElMessage.error("请建立webrtc连接");
  }

  channel.value.send(formInline.rtcmessage);

  formInline.rtcmessageRes = undefined;
};

const onRemoteOffer = async (fromUid, offer) => {
  /**
   * 设置远程描述，其中的参数是远程端发送来的消息。通过调用这个方法，本地端告诉浏览器要接收来自远程端的媒体流，并开始建立连接的过程。
   */
  localRtcPc.setRemoteDescription(offer);
  /**
   * 创建一个Answer SDP，并设置为本地描述。
   * 创建Answer SDP的过程与创建Offer SDP类似，但Answer SDP的生成过程需要考虑一些额外的因素，例如网络条件、服务质量等。
   * 创建Answer SDP后，通过调用setLocalDescription()方法将其设置为本地描述，并将其发送给远程端。
   * 这样，远程端就可以通过Answer SDP建立与本地端的连接。
   */
  let answer = await localRtcPc.createAnswer();
  /**
   *设置被地描述 其参数是本地端创建的SDP answer
   * 设置本地描述的过程与设置远程描述的过程类似，但设置本地描述的目的是告诉浏览器，本地端已经准备好接收来自远程端的媒体流，并开始建立连接的过程。
   */
  await localRtcPc.setLocalDescription(answer);
  let params = {
    targetUid: fromUid,
    userId: getParams("userId"),
    answer: answer,
  };
  linkSocket.value.emit("answer", params);
};

const onRemoteAnswer = async (formUid, answer) => {
  await localRtcPc.setRemoteDescription(answer);
};

const sendMsgToOne = (event, params) => {};

/**
 * 改变视频传输的比特率
 * @returns {Promise<void>}
 */
const changeBitRate = () => {
  console.log("changeBitRate", localRtcPc.value);
  /**
   * getSenders()返回一个包含所有的发送器的数组
   */
  const senders = localRtcPc.value.getSenders();
  const send = senders.find((s) => s.track.kind === "video");
  const parameters = send.getParameters();
  // 设置视频编码的最大比特率
  parameters.encodings[0].maxBitrate = 1 * 1000 * 1024;
  send.setParameters(parameters);
};

/**
 * 打开摄像头
 */
const openVideoOrNot = () => {
  const senders = localRtcPc.value.getSenders();
  const send = senders.find((s) => s.track.kind === "video");
  send.track.enabled = !send.track.enabled; //控制视频是否显示
};

/**
 * 获取分享屏幕的媒体流
 *@returns {Promise<void>}
 */
const getShareMedia = async () => {
  const constraints = {
    video: {
      width: 1920,
      height: 1080,
    },
    audio: true,
  };

  if (window.stream) {
    window.stream.getTracks().forEach(function (track) {
      track.stop();
    });
  }

  return await navigator.mediaDevices
    .getDisplayMedia(constraints)
    .catch(handleError);
};

const streamInfo = (domId) => {
  let video = document.getElementById(dmoId);
  console.log("video.srcObject", video.srcObject);
};

const getStats = () => {
  const senders = localRtcPc.value.getSenders();
  const send = senders.find((s) => s.track.kind === "video");
  console.log("send", send.getParameters().encodings);
  let lastResultForStats; //上次计算结果
  setInterval(() => {
    localRtcPc.value.getStats().then((res) => {
      console.log("res", res);
      res.forEach((report) => {
        let bytes;
        let headerBytes;
        let packets;

        //   出口宽带 outbound-rtp 入口宽带 inbound-rtp
        if (report.type === "outbound-rtp" && report.kind === "video") {
          const now = report.timestamp;
          bytes = report.bytesSent;
          headerBytes = report.headerBytesSent;
          packets = report.packetsSent;
          console.log(
            "bytes, headerBytes, packets",
            bytes,
            headerBytes,
            packets
          );
          if (lastResultForStats && lastResultForStats.has(report.id)) {
            let bf = bytes - lastResultForStats.get(report.id).bytesSent;
            let hbf =
              headerBytes - lastResultForStats.get(report.id).headerBytesSent;
            let pacf = packets - lastResultForStats.get(report.id).packetsSent;
            let t = now - lastResultForStats.get(report.id).timestamp;

            const bitrate = Math.floor((8 * bf) / t);
            const headerrate = Math.floor((8 * hbf) / t);
            const packetrate = Math.floor((1000 * pacf) / t);
            console.log(
              `Bitrate ${bitrate} kbps, overhead ${headerrate} kbps, ${packetrate} packets/second`
            );
          }
        }
      });

      lastResultForStats = res;
    });
  }, 4000);
};

onMounted(() => {
  /**
   * 判断是否存在userId
   * 存在则执行init函数
   */
  if (getParams("userId")) {
    init(getParams("userId"), getParams("roomId"), getParams("userId"));
  }
});
</script>

<style lang="scss" scoped>
.webrtc-demo {
  padding: 20px;
  position: relative;
}

#localdemo01 {
  width: 300px;
  height: 200px;
}
#remoteVideo01 {
  width: 300px;
  height: 200px;
}
</style>
